/*=========================================================================

  Library:   CFS

  Copyright (c) Kitware Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0.txt

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

=========================================================================*/

// Qt includes
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QMetaEnum>
#include <QMetaType>
#include <QMutexLocker>
#include <QPointer>
#include <QStringList>
#include <QThread>

// CFS includes
#include "CFSErrorLogContext.h"
#include "CFSErrorLogAbstractModel.h"
#include "CFSErrorLogAbstractMessageHandler.h"
#include "CFSFileLogger.h"


// --------------------------------------------------------------------------
// CFSErrorLogAbstractModelPrivate

// --------------------------------------------------------------------------
class CFSErrorLogAbstractModelPrivate
{
  Q_DECLARE_PUBLIC(CFSErrorLogAbstractModel);
protected:
  CFSErrorLogAbstractModel* const q_ptr;
public:
  CFSErrorLogAbstractModelPrivate(CFSErrorLogAbstractModel& object);
  ~CFSErrorLogAbstractModelPrivate();

  void init(QAbstractItemModel* itemModel);

  void setMessageHandlerConnection(CFSErrorLogAbstractMessageHandler * msgHandler, bool asynchronous);

  QAbstractItemModel* ItemModel;

  QHash<QString, CFSErrorLogAbstractMessageHandler*> RegisteredHandlers;

  CFSErrorLogLevel::LogLevels CurrentLogLevelFilter;

  bool LogEntryGrouping;
  bool AsynchronousLogging;
  bool AddingEntry;

  CFSErrorLogLevel ErrorLogLevel;

  CFSErrorLogTerminalOutput StdErrTerminalOutput;
  CFSErrorLogTerminalOutput StdOutTerminalOutput;

  CFSFileLogger FileLogger;
  QString FileLoggingPattern;
};

// --------------------------------------------------------------------------
// CFSErrorLogAbstractModelPrivate methods

// --------------------------------------------------------------------------
CFSErrorLogAbstractModelPrivate::CFSErrorLogAbstractModelPrivate(CFSErrorLogAbstractModel& object)
  : q_ptr(&object)
{
  qRegisterMetaType<CFSErrorLogContext>("CFSErrorLogContext");
  this->LogEntryGrouping = false;
  this->AsynchronousLogging = true;
  this->AddingEntry = false;
  this->FileLogger.setEnabled(false);
  this->FileLoggingPattern = "[%{level}][%{origin}] %{timestamp} [%{category}] (%{file}:%{line}) - %{msg}";
}

// --------------------------------------------------------------------------
CFSErrorLogAbstractModelPrivate::~CFSErrorLogAbstractModelPrivate()
{
  foreach(const QString& handlerName, this->RegisteredHandlers.keys())
    {
    CFSErrorLogAbstractMessageHandler * msgHandler =
        this->RegisteredHandlers.value(handlerName);
    Q_ASSERT(msgHandler);
    msgHandler->setEnabled(false);
    delete msgHandler;
    }
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModelPrivate::init(QAbstractItemModel* itemModel)
{
  Q_Q(CFSErrorLogAbstractModel);
  itemModel->setParent(q);
  q->setDynamicSortFilter(true);
  //
  // WARNING - Using a QSortFilterProxyModel slows down the insertion of rows by a factor 10
  //
  q->setSourceModel(itemModel);
  q->setFilterKeyColumn(CFSErrorLogAbstractModel::LogLevelColumn);

  this->ItemModel = itemModel;
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModelPrivate::setMessageHandlerConnection(
    CFSErrorLogAbstractMessageHandler * msgHandler, bool asynchronous)
{
  Q_Q(CFSErrorLogAbstractModel);

  msgHandler->disconnect();

  QObject::connect(msgHandler,
        SIGNAL(messageHandled(QDateTime,QString,CFSErrorLogLevel::LogLevel,QString,CFSErrorLogContext,QString)),
        q, SLOT(addEntry(QDateTime,QString,CFSErrorLogLevel::LogLevel,QString,CFSErrorLogContext,QString)),
        asynchronous ? Qt::QueuedConnection : Qt::BlockingQueuedConnection);
}

// --------------------------------------------------------------------------
// CFSErrorLogAbstractModel methods

//------------------------------------------------------------------------------
CFSErrorLogAbstractModel::CFSErrorLogAbstractModel(QAbstractItemModel* itemModel, QObject * parentObject)
  : Superclass(parentObject)
  , d_ptr(new CFSErrorLogAbstractModelPrivate(*this))
{
  Q_D(CFSErrorLogAbstractModel);

  d->init(itemModel);
}

//------------------------------------------------------------------------------
CFSErrorLogAbstractModel::~CFSErrorLogAbstractModel()
{
}

//------------------------------------------------------------------------------
bool CFSErrorLogAbstractModel::registerMsgHandler(CFSErrorLogAbstractMessageHandler * msgHandler)
{
  Q_D(CFSErrorLogAbstractModel);
  if (!msgHandler)
    {
    return false;
    }
  if (d->RegisteredHandlers.keys().contains(msgHandler->handlerName()))
    {
    return false;
    }

  d->setMessageHandlerConnection(msgHandler, d->AsynchronousLogging);

  msgHandler->setTerminalOutput(CFSErrorLogTerminalOutput::StandardError, &d->StdErrTerminalOutput);
  msgHandler->setTerminalOutput(CFSErrorLogTerminalOutput::StandardOutput, &d->StdOutTerminalOutput);

  d->RegisteredHandlers.insert(msgHandler->handlerName(), msgHandler);
  return true;
}

//------------------------------------------------------------------------------
QStringList CFSErrorLogAbstractModel::msgHandlerNames()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->RegisteredHandlers.keys();
}

//------------------------------------------------------------------------------
bool CFSErrorLogAbstractModel::msgHandlerEnabled(const QString& handlerName) const
{
  Q_D(const CFSErrorLogAbstractModel);
  if (!d->RegisteredHandlers.keys().contains(handlerName))
    {
    return false;
    }
  return d->RegisteredHandlers.value(handlerName)->enabled();
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setMsgHandlerEnabled(const QString& handlerName, bool enabled)
{
  Q_D(CFSErrorLogAbstractModel);
  if (!d->RegisteredHandlers.keys().contains(handlerName))
    {
//    qCritical() << "Failed to enable/disable message handler " << handlerName
//                << "-  Handler not registered !";
    return;
    }
  d->RegisteredHandlers.value(handlerName)->setEnabled(enabled);
}

//------------------------------------------------------------------------------
QStringList CFSErrorLogAbstractModel::msgHandlerEnabled() const
{
  Q_D(const CFSErrorLogAbstractModel);
  QStringList msgHandlers;
  foreach(const QString& handlerName, d->RegisteredHandlers.keys())
    {
    if (d->RegisteredHandlers.value(handlerName)->enabled())
      {
      msgHandlers << handlerName;
      }
    }
  return msgHandlers;
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setMsgHandlerEnabled(const QStringList& handlerNames)
{
  foreach(const QString& handlerName, handlerNames)
    {
    this->setMsgHandlerEnabled(handlerName, true);
    }
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::enableAllMsgHandler()
{
  this->setAllMsgHandlerEnabled(true);
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::disableAllMsgHandler()
{
  this->setAllMsgHandlerEnabled(false);
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setAllMsgHandlerEnabled(bool enabled)
{
  Q_D(CFSErrorLogAbstractModel);
  foreach(const QString& msgHandlerName, d->RegisteredHandlers.keys())
    {
    this->setMsgHandlerEnabled(msgHandlerName, enabled);
    }
}

//------------------------------------------------------------------------------
CFSErrorLogTerminalOutput::TerminalOutputs CFSErrorLogAbstractModel::terminalOutputs()const
{
  Q_D(const CFSErrorLogAbstractModel);
  CFSErrorLogTerminalOutput::TerminalOutputs currentTerminalOutputs;
  currentTerminalOutputs |= d->StdErrTerminalOutput.enabled() ? CFSErrorLogTerminalOutput::StandardError : CFSErrorLogTerminalOutput::None;
  currentTerminalOutputs |= d->StdOutTerminalOutput.enabled() ? CFSErrorLogTerminalOutput::StandardOutput : CFSErrorLogTerminalOutput::None;
  return currentTerminalOutputs;
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setTerminalOutputs(
    const CFSErrorLogTerminalOutput::TerminalOutputs& terminalOutput)
{
  Q_D(CFSErrorLogAbstractModel);
  d->StdErrTerminalOutput.setEnabled(terminalOutput & CFSErrorLogTerminalOutput::StandardOutput);
  d->StdOutTerminalOutput.setEnabled(terminalOutput & CFSErrorLogTerminalOutput::StandardError);
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::addEntry(const QDateTime& currentDateTime, const QString& threadId,
                                CFSErrorLogLevel::LogLevel logLevel,
                                const QString& origin, const CFSErrorLogContext &context, const QString &text)
{
  Q_D(CFSErrorLogAbstractModel);

  if (d->AddingEntry)
    {
    return;
    }

  d->AddingEntry = true;

  QString timeFormat("dd.MM.yyyy hh:mm:ss");

  bool groupEntry = false;
  if (d->LogEntryGrouping)
    {
    int lastRowIndex = d->ItemModel->rowCount() - 1;

    QString lastRowThreadId = this->logEntryData(lastRowIndex, Self::ThreadIdColumn).toString();
    bool threadIdMatched = threadId == lastRowThreadId;

    QString lastRowLogLevel = this->logEntryData(lastRowIndex, Self::LogLevelColumn).toString();
    bool logLevelMatched = d->ErrorLogLevel(logLevel) == lastRowLogLevel;

    QString lastRowOrigin = this->logEntryData(lastRowIndex, Self::OriginColumn).toString();
    bool originMatched = origin == lastRowOrigin;

    QDateTime lastRowDateTime =
        QDateTime::fromString(this->logEntryData(lastRowIndex, Self::TimeColumn).toString(), timeFormat);
    int groupingIntervalInMsecs = 1000;
    bool withinGroupingInterval = lastRowDateTime.time().msecsTo(currentDateTime.time()) <= groupingIntervalInMsecs;

    groupEntry = threadIdMatched && logLevelMatched && originMatched && withinGroupingInterval;
    }

  if (!groupEntry)
    {
    this->addModelEntry(
      currentDateTime.toString(timeFormat), threadId, d->ErrorLogLevel(logLevel), origin, text);
    }
  else
    {
    // Retrieve description associated with last row
    QModelIndex lastRowDescriptionIndex =
        d->ItemModel->index(d->ItemModel->rowCount() - 1, CFSErrorLogAbstractModel::DescriptionColumn);

    QStringList updatedDescription;
    updatedDescription << lastRowDescriptionIndex.data(CFSErrorLogAbstractModel::DescriptionTextRole).toString();
    updatedDescription << text;

    d->ItemModel->setData(lastRowDescriptionIndex, updatedDescription.join("\n"),
                                 CFSErrorLogAbstractModel::DescriptionTextRole);

    // Append '...' to displayText if needed
    QString displayText = lastRowDescriptionIndex.data().toString();
    if (!displayText.endsWith("..."))
      {
      d->ItemModel->setData(lastRowDescriptionIndex, displayText.append("..."), Qt::DisplayRole);
      }
    }

  d->AddingEntry = false;

  QString fileLogText = d->FileLoggingPattern;
  fileLogText.replace("%{level}", d->ErrorLogLevel(logLevel).toUpper());
  fileLogText.replace("%{timestamp}", currentDateTime.toString(timeFormat));
  fileLogText.replace("%{origin}", origin);
  fileLogText.replace("%{pid}", QString("%1").arg(QCoreApplication::applicationPid()));
  fileLogText.replace("%{threadid}", threadId);
  fileLogText.replace("%{function}", context.Function);
  fileLogText.replace("%{line}", QString("%1").arg(context.Line));
  fileLogText.replace("%{file}", context.File);
  fileLogText.replace("%{category}", context.Category);
  fileLogText.replace("%{msg}", context.Message);
  d->FileLogger.logMessage(fileLogText.trimmed());

  emit this->entryAdded(logLevel);
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::clear()
{
  Q_D(CFSErrorLogAbstractModel);
  d->ItemModel->removeRows(0, d->ItemModel->rowCount());
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::filterEntry(const CFSErrorLogLevel::LogLevels& logLevel,
                                   bool disableFilter)
{
  Q_D(CFSErrorLogAbstractModel);

  QStringList patterns;
  if (!this->filterRegExp().pattern().isEmpty())
    {
    patterns << this->filterRegExp().pattern().split("|");
    }
  patterns.removeAll(d->ErrorLogLevel(CFSErrorLogLevel::None));

//  foreach(QString s, patterns)
//    {
//    std::cout << "pattern:" << qPrintable(s) << std::endl;
//    }

  QMetaEnum logLevelEnum = d->ErrorLogLevel.metaObject()->enumerator(0);
  Q_ASSERT(QString("LogLevel").compare(logLevelEnum.name()) == 0);

  // Loop over enum values and append associated name to 'patterns' if
  // it has been specified within 'logLevel'
  for (int i = 1; i < logLevelEnum.keyCount(); ++i)
    {
    int aLogLevel = logLevelEnum.value(i);
    if (logLevel & aLogLevel)
      {
      QString logLevelAsString = d->ErrorLogLevel(static_cast<CFSErrorLogLevel::LogLevel>(aLogLevel));
      if (!disableFilter)
        {
        patterns << logLevelAsString;
        d->CurrentLogLevelFilter |= static_cast<CFSErrorLogLevel::LogLevels>(aLogLevel);
        }
      else
        {
        patterns.removeAll(logLevelAsString);
        d->CurrentLogLevelFilter ^= static_cast<CFSErrorLogLevel::LogLevels>(aLogLevel);
        }
      }
    }

  if (patterns.isEmpty())
    {
    // If there are no patterns, let's filter with the None level so that
    // all entries are filtered out.
    patterns << d->ErrorLogLevel(CFSErrorLogLevel::None);
    }

  bool filterChanged = true;
  QStringList currentPatterns = this->filterRegExp().pattern().split("|");
  if (currentPatterns.count() == patterns.count())
    {
    foreach(const QString& p, patterns)
      {
      currentPatterns.removeAll(p);
      }
    filterChanged = currentPatterns.count() > 0;
    }

  this->setFilterRegExp(patterns.join("|"));

  if (filterChanged)
    {
    emit this->logLevelFilterChanged();
    }
}

//------------------------------------------------------------------------------
CFSErrorLogLevel::LogLevels CFSErrorLogAbstractModel::logLevelFilter()const
{
  Q_D(const CFSErrorLogAbstractModel);
  QMetaEnum logLevelEnum = d->ErrorLogLevel.metaObject()->enumerator(0);
  Q_ASSERT(QString("LogLevel").compare(logLevelEnum.name()) == 0);

  CFSErrorLogLevel::LogLevels filter = CFSErrorLogLevel::Unknown;
  foreach(const QString& filterAsString, this->filterRegExp().pattern().split("|"))
    {
    filter |= static_cast<CFSErrorLogLevel::LogLevels>(logLevelEnum.keyToValue(filterAsString.toLatin1()));
    }
  return filter;
}

//------------------------------------------------------------------------------
bool CFSErrorLogAbstractModel::logEntryGrouping()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->LogEntryGrouping;
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setLogEntryGrouping(bool value)
{
  Q_D(CFSErrorLogAbstractModel);
  d->LogEntryGrouping = value;
}

//------------------------------------------------------------------------------
bool CFSErrorLogAbstractModel::asynchronousLogging()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->AsynchronousLogging;
}

//------------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setAsynchronousLogging(bool value)
{
  Q_D(CFSErrorLogAbstractModel);
  if (d->AsynchronousLogging == value)
    {
    return;
    }

  foreach(const QString& handlerName, d->RegisteredHandlers.keys())
    {
    d->setMessageHandlerConnection(
          d->RegisteredHandlers.value(handlerName), value);
    }

  d->AsynchronousLogging = value;
}

// --------------------------------------------------------------------------
QString CFSErrorLogAbstractModel::filePath()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->FileLogger.filePath();
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setFilePath(const QString& filePath)
{
  Q_D(CFSErrorLogAbstractModel);
  return d->FileLogger.setFilePath(filePath);
}

// --------------------------------------------------------------------------
int CFSErrorLogAbstractModel::numberOfFilesToKeep()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->FileLogger.numberOfFilesToKeep();
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setNumberOfFilesToKeep(int value)
{
  Q_D(CFSErrorLogAbstractModel);
  return d->FileLogger.setNumberOfFilesToKeep(value);
}

// --------------------------------------------------------------------------
bool CFSErrorLogAbstractModel::fileLoggingEnabled()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->FileLogger.enabled();
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setFileLoggingEnabled(bool value)
{
  Q_D(CFSErrorLogAbstractModel);
  d->FileLogger.setEnabled(value);
}

// --------------------------------------------------------------------------
QString CFSErrorLogAbstractModel::fileLoggingPattern()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->FileLoggingPattern;
}

// --------------------------------------------------------------------------
void CFSErrorLogAbstractModel::setFileLoggingPattern(const QString& value)
{
  Q_D(CFSErrorLogAbstractModel);
  d->FileLoggingPattern = value;
}

// --------------------------------------------------------------------------
QVariant CFSErrorLogAbstractModel::logEntryData(int row, int column, int role) const
{
  Q_D(const CFSErrorLogAbstractModel);
  if (column < 0 || column > Self::MaxColumn
      || row < 0 || row > this->logEntryCount())
    {
    return QVariant();
    }
  QModelIndex rowDescriptionIndex = d->ItemModel->index(row, column);
  return rowDescriptionIndex.data(role);
}

// --------------------------------------------------------------------------
QString CFSErrorLogAbstractModel::logEntryDescription(int row) const
{
  return this->logEntryData(row, Self::DescriptionColumn, Self::DescriptionTextRole).toString();
}

// --------------------------------------------------------------------------
int CFSErrorLogAbstractModel::logEntryCount()const
{
  Q_D(const CFSErrorLogAbstractModel);
  return d->ItemModel->rowCount();
}
